home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Celestin Apprentice 5
/
Apprentice-Release5.iso
/
Source Code
/
Libraries
/
VideoToolbox 96.06.15
/
VideoToolboxSources
/
Luminance.doc
< prev
next >
Wrap
Text File
|
1995-09-26
|
24KB
|
482 lines
#if 0
Luminance.doc
This file documents Luminance.c.
HISTORY:
3/11/92 dgp added prototypes to supplement the explanations of the routines.
3/24/94 dgp added LToE, LToEOrdered, and EToL.
10/17/94 dgp updated, in response to queries by Lan Zhang.
INTRODUCTION:
The purpose of these subroutines is accurate control of contrast of visual
stimuli for vision experiments. These subroutines set up the Color Lookup Table
(CLUT) of a video framestore on the Mac II (e.g. the Apple Mac II Color Video
Card) to properly drive a monochrome monitor (e.g. the Apple High Resolution
Monochrome Monitor) via an Institute for Sensory Research (ISR) Video
Attenuator. The ISR Video Attenuator consists of resistive dividers which
attenuate the three color signal from the video framestore by different
amounts and combine them to produce one monochrome signal.
The video attenuator and the theory behind the algorithms described here are
the topic of a paper:
D.G. Pelli and L. Zhang (1991) Accurate control of contrast on microcomputer displays.
Vision Research, 31:1337-1360.
Achieving the goal of accurate control of contrast required solving five
problems:
1. The luminance of the monitor is nonlinearly related to the input voltage
which depends linearly on the triplet loaded into the CLUT. This is solved by
allowing the user to provide a polynomial (I recommend 4th order) describing the
nonlinear relationship between luminance and nominal voltage. (For computational
reasons, you must also supply a quadratic fit.) The subroutines LToV and VToL
use this polynomial to convert back and forth.
2. Affordable framestores for microcomputers have 8 bit Digital to Analog
Converters (DACs), which can only represent 256 different luminances. This is
not enough precision to produce threshold-contrast patterns, which are important
for vision experiments. This is solved by the ISR Video Attenuator which
combines the outputs of the three DACs ("red", "green", and "blue") with various
attenuations to yield one monochrome signal which can represent low contrasts
(by varying an attenuated DAC) and high contrasts (by varying an unattenuated
DAC).
3. DACs are only accurate to ± a least significant bit (LSB). The Brooktree DACs
used by Apple and RasterOps are specified to have a ±1 LSB "integrated
linearity" error, which means that the voltage produced by any number will
deviate from a line connecting the 0 and 255 points by at most 1 LSB, which is
defined as 1/255 times the voltage difference between 0 and 255. I assume a
slightly stronger restriction will hold, that the difference between any two
settings will be in error by at most one LSB. So it is important to allow the
user to specify what range of luminances will be used to present a particular
stimulus and to fix the coarsest DACs (i.e. those with the least attenuation in
the ISR Video Attenuator) and only vary the fine DACs to produce the specified
luminances. The range setting is done by SetLuminanceRange() which is called
automatically by SetLuminance() and SetLuminances().
4. Apple's software environment for graphics, QuickDraw, isn't designed for
vision experiments. The facilities for creating images are fine, but the control
of the CLUTs provided by the Palette and Color Managers is unsatisfactory for
vision research because a lot happens behind your back. In particular the
Palette Manager likes to maintain a consistent color environment across the
entire desktop, which includes all your screens. Well, in vision experiments we
generally want to treat each screen as a totally separate device, unaffected by
what we do to the other screens. This is solved by the routine GDSetEntries()
which makes a low-level call to the video driver to change the CLUT directly
without QuickDraw's knowledge. (This will work with any video card that works
with the Mac II.) The routine LoadLuminances(), which is used below, is just a
convenient glue routine for calling GDSetEntries().
5. Apple specifies that the video driver should silently implement gamma
correction to attempt to correct for the display's nonlinear relation between
luminance and input voltage. We don't want this because this hidden gamma
correction loses precision and interferes with the operation of the ISR Video
Attenuator, which will seem to operate NONlinearly if this gamma correction
takes place unbeknownst to us. This is solved by the routine GDLinearGamma()
which makes a low-level call to the video driver and loads a linear gamma table,
i.e. no correction.
SUMMARY:
double EToL(LuminanceRecord *LP,int entry);
Returns the luminance associated with the particular entry.
int LToE(LuminanceRecord *LP,double L,int firstEntry,int lastEntry);
Returns the index of the table entry in specified range with luminance closest to L.
int LtoEOrdered(LuminanceRecord *LP,double L,int firstEntry,int lastEntry);
Returns the index of the table entry in specified range with luminance closest to L.
Runs fast by assuming an ordered table from firstEntry to lastEntry, either
increasing or decreasing.
double SetLuminance(GDHandle device,LuminanceRecord *LP
,int entry,double luminance
,double lowLuminance,double highLuminance)
Set one entry in the ColorSpec table (and the CLUT if device is not NULL) to
a specified luminance. It's ok for lowLuminance to be greater than highLuminance.
SetLuminance() sets a single entry to the specified luminance. You must also
indicate the luminance range that you are working over, to allow optimal choice
of which dacs to fix, etc., to yield minimum error in relative luminance.
SetLuminances() does its work by making a call to SetLuminance() for each entry.
SetLuminance takes 1 ms to set up the range (by calling SetLuminanceRange), which
it only has to do once. Repeated calls to SetLuminance with the same range will
not cause re-computation of the range. Once the range is set, SetLuminance takes
about 0.1 ms. If SetLuminance is too slow for a real-time application you can tell
it to just compute, but not load the new ColorSpec table (just supply NULL in place
of device), and you can then quickly load your ColorSpec table into the CLUT
later by calling LoadLuminances().
double SetLuminancesAndRange(GDHandle device,LuminanceRecord *LP
,int firstEntry,int lastEntry
,double firstLuminance,double lastLuminance
,double lowLuminance,double highLuminance)
/*
Set a series of entries in the ColorSpec table (and the CLUT if device is not NULL)
to a linear sequence of luminances.
Uses last two arguments to set the luminance range of interest.
*/
double SetLuminances(GDHandle device,LuminanceRecord *LP
,int firstEntry,int lastEntry
,double firstLuminance,double lastLuminance)
/*
Set a series of entries in the ColorSpec table (and the CLUT if device is not NULL)
to a linear sequence of luminances. Assume this is the entire luminance range of
interest.
*/
SetLuminances() and SetLuminancesAndRange() both produce a linear relationship
between CLUT entry and luminance over the range firstLuminance to lastLuminance,
with minimum error relative to any luminance in the range lowLuminance to
highLuminance. (We don't care about a small error in mean luminance.)
(SetLuminances, which doesn't have these arguments, uses firstLuminance and
lastLuminance to set this range.) The three DACs are linear, and the video
attenuator will combine them linearly yielding a voltage V at the display input.
This allows you to compute your image (e.g. a sinewave grating) and loaded to
the frame store as a full scale linear function, i.e. with values ranging
0..255. Linearization of the display and setting of contrast to whatever you
want are both accomplished by one call to SetLuminances(). The contrast can be
changed by calling SetLuminances() again, now with a larger or smaller luminance
range. If SetLuminances is too slow for a real-time application (its
computations take about 8-30 ms to compute a whole 256-entry CLUT) you can tell
it to just compute, but not load the new ColorSpec table (just supply NULL in
place of device), and you can then quickly load your ColorSpec table into
the CLUT later by calling LoadLuminances().
double GetLuminance(GDHandle device,LuminanceRecord *LP,int entry)
/*
If device is not NULL then examines one entry in the actual CLUT, otherwise
examines the ColorSpec table contained in *LP, and in either case returns the
luminance that will be produced. Supplying an illegal entry value results in a
returned value of -INF.
*/
GetLuminance() returns the luminance of a single entry. If device is not NULL then
GetLuminance will make a low-level call to the video driver to determine what is in that
CLUT entry in the actual hardware, and will return the luminance that is expected
to result given the channel gains and luminance nonlinearity described in your
LuminanceRecord. If device is NULL then GetLuminance
returns the luminance corresponding to the entry in the ColorSpec table in your
LuminanceRecord.
void IncrementLuminance(GDHandle device,LuminanceRecord *LP,int entry)
/*
Make smallest possible increase of the luminance of one entry in the ColorSpec table.
This is a way to figure out what is the lowest contrast that you can produce.
*/
void LoadLuminances(GDHandle device, LuminanceRecord *LP,
int firstEntry, int lastEntry)
/*
This just calls GDSetEntries() to load your ColorSpec table into the CLUT of
your screen device. It is here simply to provide a cosmetic match to the
call to SetLuminances(), for which loading the CLUT is optional.
Note: if you prefer, instead of LP you may send just the address of
a ColorSpec table, cast to (LuminanceRecord *), since a LuminanceRecord
begins with a ColorSpec table.
Does nothing if device==NULL or the device has no driver.
*/
double GetV(GDHandle device,LuminanceRecord *LP,int entry);
double VToL(LuminanceRecord *LP,double V);
double LToV(LuminanceRecord *LP,double L);
double LToL(LuminanceRecord *LP,double L);
Most users will never have any reason to use these routines. They are useful
primarily in error analysis of the Luminance.c package.
EXAMPLES:
double tolerance,luminance,meanLuminance,c;
int firstEntry,lastEntry,entry;
/* load CLUT with linear luminance range, immediately */
tolerance=SetLuminances(device,&LR,0,255,(1.-c)*meanLuminance,(1+c)*meanLuminance);
/* load ColorSpec table with linear luminance range, then load CLUT */
tolerance=SetLuminances(NULL,&LR,0,255,(1.-c)*meanLuminance,(1+c)*meanLuminance);
LoadLuminances(device,&LR,firstEntry,lastEntry);
/* load ColorSpec table with sinusoidal luminance range, then load CLUT */
for(entry=0;entry<256;entry++) {
luminance = meanLuminance*(1.0+c*sin(2.0*3.14159265*entry/256.));
tolerance=SetLuminance(NULL,&LR,entry,luminance,(1.-c)*meanLuminance,(1+c)*meanLuminance);
}
LoadLuminances(device,&LR,firstEntry,lastEntry);
/* Examine an entry in the ColorSpec table or CLUT */
luminance=GetLuminance(device,&LR,entry); /* in CLUT */
luminance=GetLuminance(NULL,&LR,entry); /* in ColorSpec table */
NOTES:
The argument firstEntry must not exceed lastEntry. They may be equal.
The returned value, tolerance, is an estimate of the largest possible error in
the luminance DIFFERENCES, taking into account the precision, accuracy (assumed
to be ±1 least significant bit), and range of the DACs. (Note the returned
tolerance tells you about errors in the luminance DIFFERENCES on the display.
The error in ABSOLUTE luminance of the display is expected to be at most ±1 step
of all the DACs, i.e. one part in 255 of full scale.)
There are no restrictions at all on firstLuminance and lastLuminance. It is
reasonable to request an impossibly large luminance range, e.g. from a negative
luminance up to twice the maximum luminance. SetLuminances will produce the
closest possible approximation. The luminance of each entry will be clipped to
fit in the range of possible luminances. Thus you will obtain the requested
contrast for pixel values that aren't clipped. You can detect the clipping by
the fact that the returned tolerance will be very large.
A common mistake in C programming is to use a pointer that supposedly points to
a structure, without ever allocating said structure. Consider the
LuminanceRecord. The following declaration and call will be accepted by the
compiler, but will usually have disastrous consequences:
LuminanceRecord *LP;
SetLuminances(...,LP,...); /* Bad! */
The problem is that you never allocated a LuminanceRecord, only a pointer. When
SetLuminances starts storing information in what it thinks is a LuminanceRecord it
will actually be writing over a random part of memory. What you should do is:
LuminanceRecord LR;
SetLuminances(...,&LR,...); /* Good */
Or you can do this:
LuminanceRecord LR,*LP;
LP=&LR;
SetLuminances(...,LP,...); /* Good */
This second technique is a better habit, since if you want to put some of your code into
a subroutine, you'll be passing the pointer, not the structure itself, so you might as well
get used to using the pointer.
Before using SetLuminance or SetLuminances you must initialize your LuminanceRecord.
You should do that by reading in,
LuminanceRecord LR;
ReadLuminanceRecord("LuminanceRecord2.h",&LR,0);
or #including a file with all the parameters describing your monitor.
LuminanceRecord LR;
#include "LuminanceRecord2.h"
Here's an example of the contents of a LuminanceRecord1.h file, as produced by the
program CalibrateLuminance. You must use CalibrateLuminance to calibrate
your own framestore, ISR Video Attenuator, and display.
LR.screen=1; /* device=GetScreenDevice(LR.screen); */
/* Caution: the screen number used here and in GetScreen Device is NOT the same as */
/* displayed by the Monitors cdev in the Control Panel. Sorry. The most obvious difference */
/* is that GetScreenDevice always assigns 0 to the main screen, the one with the menu bar. */
LR.date="3:09 PM Friday, October 16, 1992";
LR.id="5111767";
LR.name="signal";
LR.notes="manoj, lights off, photometer 1 inch from screen";
LR.dpi=76.0; /* pixels per inch */
LR.Hz=66.67; /* frames per second */
LR.units="cd/m^2";
/* coefficients of polynomial fit */
LR.coefficients=9; /* # of coefficients in polynomial fit */
/* L(V)=p[0]+p[1]*V+p[2]*V*V+ . . . ±polynomialError */
LR.p[0]=1.28201e-14;
LR.p[1]=8.25776e-13;
LR.p[2]=5.00064e-11;
LR.p[3]=2.51138e-09;
LR.p[4]=7.98055e-08;
LR.p[5]=-2.04184e-09;
LR.p[6]=1.79181e-11;
LR.p[7]=-6.3791e-14;
LR.p[8]=8.10888e-17;
LR.polynomialError= 0.2263; /* RMS error of fit */
/* coefficients of quadratic fit */
/* L(V)=q[0]+q[1]*V+q[2]*V*V±quadraticError */
LR.q[0]=4.86285;
LR.q[1]=-0.201266;
LR.q[2]=0.00125998;
LR.quadraticError= 2.5493; /* RMS error of fit */
/* coefficients of power law fit */
/* L(V)=power[0]+Rectify(power[1]+power[2]*V)^power[3]±powerError */
/* where Rectify(x)=x if x≥0, and Rectify(x)=0 otherwise */
/* Pelli & Zhang (1991) Eqs.9&10 use symbols v=V/255, alpha=power[0], beta=power[1], kappa=power[2]*255, gamma=power[3] */
LR.power[0]=-0.0615421;
LR.power[1]=-3.38554;
LR.power[2]=0.0305017;
LR.power[3]=2.48866;
LR.powerError= 0.2616; /* RMS error of fit */
/* coefficients of power law fit, with fixed exponent */
/* L(V)=fixedPower[0]+Rectify(fixedPower[1]+fixedPower[2]*V)^fixedPower[3]±fixedPowerError */
LR.fixedPower[0]=-0.764058;
LR.fixedPower[1]=-2.96785;
LR.fixedPower[2]=0.0310164;
LR.fixedPower[3]= 2.28;
LR.fixedPowerError= 1.1986; /* RMS error of fit */
LR.r=0.0282406;
LR.g=0.149883;
LR.b=0.821877;
LR.gainAccuracy=-0.0337905;
LR.gm=3.85543; /* The monitor's contrast gain. */
LR.VMin= 0; /* minimum value that can be loaded into DAC */
LR.VMax=255; /* maximum value that can be loaded into DAC */
LR.LMin= -0.06; /* luminance at VMin */
LR.LMax= 39.70; /* luminance at VMax */
LR.LBackground= 9.731; /* background luminance during calibration */
LR.VBackground=193; /* background number used during calibration */
LR.rangeSet=0; /* indicate that range parameters have yet to be set */
LR.L.exists=0; /* indicate that luminance table has yet to be initialized */
Note that these parameters describe fits of three functions to the gamma function.
You may omit any or all of these fits, but should document it by setting the
corresponding error terms to infinity (=1.0/0.0). If you omit all of them then
you must supply a table describing the gamma function, e.g.
LR.L._VMin=DoubleToFix(0.); /* if possible, restrict to just the monotonic range */
LR.L._VMax=DoubleToFix(255.); /* if possible, restrict to just the monotonic range */
LR.L._dV=(LR.L._VMax-LR.L._VMin)/(LUMINANCES_IN_TABLE-1);
LR.L.latestIndex=-1; /* invalid latestIndex, so hunt will start from scratch */
LR.L.exists=luminanceSet; /* mark table as valid */
LR.L._Lu[0]=DoubleToFix(0.57);
LR.L._Lu[1]=DoubleToFix(0.57);
...
LR.L._Lu[LUMINANCES_IN_TABLE-1]=DoubleToFix(101.13);
"LUMINANCES_IN_TABLE" is currently defined to be 128 in Luminance.h. Normally this
table is synthesized at run time, from one of the formulaic descriptions,
but you may prefer to supply it directly, possibly using the raw measurements,
and not bother with any fitting. The formulas are not used if the table is supplied.
To fill in the numbers above, you have to do two calibrations. (This is what the
CalibrateLuminance program does.)
0. Before starting, call GDLinearGamma(). (Note that GDOpenWindow
automatically calls GDLinearGamma for you.) All calibrations should be done
with the ISR Video Attenuator in place.
1. Measure the luminance versus "voltage" nonlinearity of your monitor.
I use a separate program "CalibrateLuminance" to measure the screen luminance at values
of V from 0 to 255. For this calibration you load equal values into Red, Green, and Blue
lookup tables. I use quadratic, polynomial, and power law fits. See Pelli & Zhang (1991).
You must set LR.LMin and LR.LMax to the luminances at LR.VMin and LR.VMax.
The following program fragment allows you to measure the screen luminance as a function
of nominal voltage.
#include "VideoToolbox.h"
#include "Luminance.h"
void main(void)
{
LuminanceRecord LR;
int V,i,done;
double a;
GDHandle oldDevice,device;
CWindowPtr cWindow,oldCWindow;
char string[32];
#include "LuminanceRecord1.h"
GetGWorld(&oldCWindow,&oldDevice);
LR.screen=1;
device=GetScreenDevice(LR.screen); /* screen 1 */
cWindow=GDOpenWindow(device); /* Open a full-screen window with explicit colors */
SetGWorld(cWindow,device);
PmBackColor(1); /* pick a color table entry */
EraseRect(&cWindow->portRect); /* fill whole window with that color */
SetGWorld(oldCWindow,oldDevice);
LR.L._VMin=DoubleToFix(0.); /* if possible, restrict to just the strictly monotonic range */
LR.L._VMax=DoubleToFix(255.);
done=0;
for(i=0;i<LUMINANCES_IN_TABLE;i++){
LR.L._dV=ceil(FixToDouble(LR.L._VMax-LR.L._VMin)/(LUMINANCES_IN_TABLE-1));
V=FixToLong(i*LR.L._dV);
if(V>=FixToLong(LR.L._VMax)){
V=FixToLong(LR.L._VMax);
done=1;
}
LR.table[1].rgb.red =V<<8; /* Set color table entry 1. */
LR.table[1].rgb.green =V<<8;
LR.table[1].rgb.blue =V<<8;
LoadLuminances(device,&LR,1,1); /* Copy color table entry 1 to CLUT */
printf("%3d Please measure screen luminance now, in %s, and type in:",V,LR.units);
gets(string);
sscanf(string,"%lf",&a);
printf("%6.2g %s\n",a,LR.units);
LR.L._Lu[i]=DoubleToFix(a);
if(done)break;
if(LR.L._Lu[i]<=LR.L._Lu[0]){ /* restrict to just the strictly monotonic range */
LR.L._VMin=LongToFix(V);
LR.L._Lu[0]=LR.L._Lu[i];
i=0;
}
}
LR.L.latestIndex=-1; /* invalid latestIndex, so hunt will start from scratch */
LR.L.exists=luminanceSet; /* mark table as valid */
}
2. Measure the gains of the three inputs of your ISR Video Attenuator. You could
do this by using an oscilloscope to measuring the voltage at the input to the monitor.
Instead, I measure the resulting luminance and use LToV() to infer the voltage.
Vary one DAC while holding the other two fixed at 255 and measure the luminances.
Use the function LToV() to convert back to a "voltage" and compute the gain of the
varying DAC. Note that the three DACs on your framestore will generally have gains that
match to only ±5%. This calibration is measuring the overall gain of each of the three
pathways, including your DACs and the ISR Video Attenuator.
LIMITATIONS:
It is imperative that you call GDLinearGamma() at some time before using
SetLuminances. SetLuminances ASSUMES that no gamma correction takes place. You
only need to call GDLinearGamma once. It will stay that way until the next time
you restart your computer. Incidentally, GDOpenWindow() calls GDLinearGamma for
you.
The luminance calibration data that you supply to SetLuminances must also have
been collected with no gamma correction, i.e. AFTER calling GDLinearGamma.
The reason that gamma correction is not allowed is that it would result in a
nonlinear transformation of the three channels BEFORE they are combined in the
Video Attenuator.
The measurement of the gains of the three pathways must be made using YOUR
framestore. The gains of your three DACs will in general be different from each
other, and different from framestore to framestore. Brooktree guarantees the
matching of the gains of the three DACs on their chip to only ±5%.
The luminance record may include either a table of luminance calibrations. If it
is not supplied then it will be synthesized from the parameters of the
polynomial or power law fit. If the power, polynomial, or quadratic fit
parameters are not supplied then the appropriate LR.powerError,
LR.polynomialError, or LR.quadraticError field should be set to infinity
(=0.0/1.0).
The whole package is at present restricted to grayscale.
There is no provision for linearizing a color monitor. In particular it is
assumed that the the DACs are linearly combined BEFORE the display nonlinearity.
Linearizing luminance on a color display would need to allow for three different
nonlinear transformations.
EXPLANATION OF THE CODE:
Physically your framestore transforms each CLUT entry number to a voltage that
will be linearly related to the number. The three output voltages will be
combined by the ISR Video Attenuator to produce a single voltage which drives
the video monitor. Finally, the display nonlinearly transforms V to produce a
luminance L. My convention for "measuring" the "voltage" V at the input of the
monitor is that V=0 when all three DACs are set to zero and V=255 when all three
DACs are set to 255. (This will be linearly related to volts measured, with a
voltmeter, at the output of the Video Attenuator.) The virtue of the attenuator
is that it allows us to produce nonintegral values of V.
There are two givens:
1. DACs are inaccurate. A good DAC may be specified to be merely monotonic. For
purposes of computing the returned tolerance value, I follow the Brooktree DAC
specification of ±1 LSB error in integrated linearity and assume that the error
in any luminance difference is at most ±1 LSB.
2. We want to minimize the error in representing the waveform, but are not
particularly worried about the exact value of the mean luminance. Thus, if we
have DACs with different gains we may fix the coarse DACs to set the mean
luminance and vary the fine DACs to produce the linear range of luminances
requested.
Here's the strategy. We want to cover the range L0 to Ln with the smallest
possible error in L-L0.
Steps 1 to 5 happen in SetLuminanceRange:
1. Sort the DACs by gain g, where gain is defined as the change in V when
a single DAC is increased from 0 to 255, so g0+g1+g2=1. Let the gains be g0>g1>g2.
2. Transform the luminance range to a nominal voltage range lowV and highV.
3. Decide which DACs should be variable and which should be fixed,
so as the minimize the tolerance in the luminance increments.
4. Temporarily set the variable DACs to their midpoints. Now set the fixed DACs to
most accurately represent the midpoint of the nominal voltage range, (lowV+highV)/2.
5. If necessary, compute a small luminance offset LShift to bring the requested range
lowV to highV into the range attainable by the variable DACs. (This is necessary
because the centering in step 4 may not be precise enough.)
Step 6 happens in _SetLuminance:
6. Set the variable DACs in the ColorSpec entry so as to most accurately represent
L+LShift.
SetLuminances(), SetLuminancesAndRange(), and SetLuminance() all call _SetLuminance().
#endif